package com.hmdp.service.impl;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisData;
import com.hmdp.utils.SystemConstants;
import io.lettuce.core.GeoSearch;

import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.domain.geo.GeoReference;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import static com.hmdp.utils.RedisConstants.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.geo.*;
/**
 * <p>
 *  服务实现类
 * </p>
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    StringRedisTemplate stringRedisTemplate;
    @Override
    public Result queryById(Long id){
//        传入店铺id
        Shop shop=queryWithLogicalExpire(id);
//        查询商店是否存在
        System.out.println("这里是打印shop:"+shop);
        //调用 queryWithLogicalExpire 方法，传入店铺ID id，
        // 尝试从缓存中获取店铺信息。该方法可能会使用逻辑过期的方式来处理缓存数据。
        if(shop==null){
            return Result.fail("店铺不存在");
        }
        return Result.ok(shop);
//        检查 shop 是否为 null。如果为 null，说明没有找到对应的店铺信息，
//        调用 Result.fail 方法返回一个失败的结果，提示信息为“店铺不存在”。
    }
//    这段代码的主要功能是根据店铺ID查询店铺信息，优先从缓存中获取数据。
//    如果缓存中没有找到对应的店铺信息，则返回一个失败的结果；如果找到了店铺信息，
//    则返回一个成功的结果，并将店铺信息作为返回数据。
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public Shop queryWithLogicalExpire(Long id){

        Shop shop=null;
        System.out.println("queryWithLogicalExpire方法被调用了");
        //1.从redis查询商铺缓存
        //传入商店id
        String shopJson=stringRedisTemplate.opsForValue().get("cache:shop:"+id);
        //在redis中回去这个店铺key/id
        if(StrUtil.isNotBlank(shopJson)){
            //防止缓存穿透：通过 StrUtil.isNotBlank(shopJson) 判断是否缓存命中，
            // 并通过 RedisData 中的 expireTime 判断缓存是否过期。如果缓存为空或者过期，才会查询数据库。
            //2.判断是否为空
            //这里先检查reids中是否命中商店的信息,有就不执行后续的代码,如果不命中,那就执行后续代码
            RedisData redisData=JSONUtil.toBean(shopJson, RedisData.class);
            //反序列化是把json文件转化为对象，变成操作的对象
            //这行代码将从Redis中获取到的商铺数据（shopJson）反序列化成一个 RedisData 对象。
            // RedisData 是一个自定义的类，通常用来表示缓存中的数据结构，包括商铺数据本身 (data) 和过期时间 (expireTime) 等信息。
            JSONObject data=(JSONObject)redisData.getData();
            //这里获取 RedisData 中的数据部分，即商铺的具体信息。
            // 假设 RedisData 的 getData() 方法返回的数据是 Shop 类型或者可以转换为 Shop 的数据格式。
            shop=JSONUtil.toBean(data, Shop.class);
            //这行代码将上一步提取出的 data 部分（一个 JSONObject）再反序列化为 Shop 对象，表示从缓存中获取的商铺信息。
            LocalDateTime expireTime=redisData.getExpireTime();
//        获取 RedisData 中存储的缓存过期时间，这个时间表示缓存数据的有效期。
            if(expireTime.isAfter(LocalDateTime.now())){
                return shop;
//                用来检查缓存是否仍然有效。意思是：如果缓存的过期时间在当前时间之后，那么缓存的数据还有效，
//                直接返回缓存中的商铺数据。
            }
        }

        boolean isLock=tryLock("lock:shop:"+id);
        //使用tryLock方法尝试获取锁，传入锁的键（lockKey）。
        // 如果获取锁成功（isLock为true），则执行后续的逻辑。
        // 如果获取锁失败（isLock为false），则直接返回缓存中的商铺信息（shop）。
        if(isLock){
            System.out.println("我被锁住了");
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try{
                    saveShop2Redis(id,20L);
//                    保存商店到redis中,传入id和过期时间
                }catch(Exception e){
                    throw new RuntimeException(e);
                }finally {
                    //最终都会被释放
                    unlock("lock:shop:"+id);
                    // 只有一个线程去数据库查询数据并重建缓存，避免多个线程并发访问数据库。
                    // 确保在缓存重建完成或发生异常后，锁会被释放。
                }
            });
        }
        return shop;
    }
    private boolean tryLock(String key){

        Boolean flag=stringRedisTemplate.opsForValue().setIfAbsent(key,"1",1L, TimeUnit.SECONDS);
        //参数key是唯一标识
        //1:当前锁已被获取，表示该资源正在被某个进程或客户端占用。
        //30s后释放锁,防止死锁
        return BooleanUtil.isTrue(flag);
//        istrue()只有在true才会返回true，false/null返回false
    }
    private void unlock(String key){

        Boolean flag=stringRedisTemplate.delete(key);
        //删除锁
    }
    public void saveShop2Redis(Long id ,Long expireSeconds){
        //传入商店id,传入过期时间
        Shop shop=getById(id);
        RedisData redisData=new RedisData();
       // 创建一个存储商店信息的对象
        redisData.setData(shop);
//        传入商店信息,保存商店
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
//        setExpireTime是设置过期时间,
//        expireSeconds这里是20s
        // 设置过期时间
        stringRedisTemplate.opsForValue().set("cache:shop:"+id,JSONUtil.toJsonStr(redisData));
        // 转换为JSON并存储
    }
    @Override
    public Result update(Shop shop) {
        Long id=shop.getId();
        if(id==null){
            return Result.fail("店铺id不能为空");
        }
        updateById(shop);
        //更新商店信息
        stringRedisTemplate.delete("cache:shop:"+shop.getId());
        //这行代码的作用是删除与更新的商店相关的缓存数据。由于在更新数据库后，
        // 缓存中的数据已经过时，所以必须从Redis中删除旧的缓存，以便下一次请求时能重新获取最新的数据。
        return Result.ok();
    }

    @Override
    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //加入定位
        Page<Shop> page = query()
                .eq("type_id", typeId)
                .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
        return Result.ok(page.getRecords());
    }
}
